package edu.washington.cs.workflow.recommender;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import edu.washington.cs.workflow.analysis.guistructure.UIActionHandler;
import edu.washington.cs.workflow.matching.ExecutionTraceMatcher;
import edu.washington.cs.workflow.recommender.MethodSummarizer.MethodStatistic;
import edu.washington.cs.workflow.util.Utils;

/**
 * Input:
 *  a demonstrate trace
 *  one or a set of traces
 *  a matcher
 *  
 * Output:
 *  a recommended action of the last broken action
 * */
public class ReplacementActionRecommender {

	public final String oldTraceFile;
	public final List<String> newTraceFiles;
	
	protected ExecutionTraceMatcher matcher = null;
	
	public ReplacementActionRecommender(String oldTraceFile, String... newTraceFiles) {
		this(oldTraceFile, Arrays.asList(newTraceFiles));
	}
	
	public ReplacementActionRecommender(String oldTraceFile, List<String> newTraceFiles) {
		Utils.checkNotNull(oldTraceFile);
		Utils.checkNotNull(newTraceFiles);
		this.oldTraceFile = oldTraceFile;
		this.newTraceFiles = newTraceFiles;
	}
	
	public void setTraceMatcher(ExecutionTraceMatcher matcher) {
		Utils.checkNotNull(matcher);
		this.matcher = matcher;
	}
	
	
	/**
	 * Another simple action recommender
	 * */
	public List<String> recommendActionsByUniqueness() {
		MethodSummarizer summarizer = new MethodSummarizer(this.oldTraceFile, newTraceFiles.toArray(new String[0]));
		List<MethodStatistic> stats = summarizer.getRankedMethodStatistics();
		Set<String> actionSet = new LinkedHashSet<String>();
		int i = 0;
		for(MethodStatistic stat : stats) {
			actionSet.addAll(stat.actions);
			if(stat.numOfActions > 0 && i++ < 10) {
				//DEBUG option to see what is inside
				System.out.println(stat);
			}
		}
		List<String> actionList = new LinkedList<String>();
		actionList.addAll(actionSet);
		return actionList;
	}
	
	/**
	 * Alternative implementation. Not used!
	 * */
	public List<String> recommendNewActions() {
		EventTraceProcessor oldProcessor = new EventTraceProcessor(this.oldTraceFile);
		EventTraceProcessor[] newProcessors = new EventTraceProcessor[this.newTraceFiles.size()];
		for(int i = 0; i < newProcessors.length; i++) {
			newProcessors[i] = new EventTraceProcessor(this.newTraceFiles.get(i));
		}
		//iterate through old processor's action handlers
		List<UIActionHandler> reversedHandlerList = oldProcessor.getAllActionHandlers();
		Collections.reverse(reversedHandlerList);
		
		//a map maintaining an action to its weight
		Map<String, Float> weightMap = new LinkedHashMap<String, Float>();
		
		
		List<UIActionHandler> noMatchingHandlers = new LinkedList<UIActionHandler>();
		
		for(UIActionHandler handler : reversedHandlerList) {
			
			Utils.debugPrintln("--------------------");
			Utils.debugPrintln("handler: " + handler);
			
			String identicalHandler = this.getMatchedSignature(handler.methodDesc);
			String similarHandler = this.getMostSimilarSignature(handler.methodDesc);
			Set<String> identicalHandlerActions = this.queryTriggeringAction(identicalHandler, newProcessors);
			Set<String> similarHandlerActions = this.queryTriggeringAction(similarHandler, newProcessors);
			
			Utils.debugPrintln("      identical handler actions: " + identicalHandlerActions.size() + ",  " + identicalHandlerActions);
			Utils.debugPrintln("        similar handler actions: " + similarHandlerActions.size() + ",  " + similarHandlerActions);
			
			Set<String> actionsForHandlers = null;
			if(!identicalHandlerActions.isEmpty()) {
				actionsForHandlers = identicalHandlerActions;
			} else {
				if(similarHandlerActions.isEmpty()) {
					noMatchingHandlers.add(handler); //keep it
					continue; //skip this handler
				} else {
					actionsForHandlers = similarHandlerActions;
				}
			}
			//must be some handlers
			Utils.checkNotNull(actionsForHandlers);
			int num = actionsForHandlers.size();
			Float w = 1.0f / num;
			for(String action : actionsForHandlers) {
				if(weightMap.containsKey(action)) {
					if(weightMap.get(action) < w) {
						weightMap.put(action, w); //update the weight
					}
				} else {
					weightMap.put(action, w);
				}
			}
			
			List<String> invokedMethods = oldProcessor.getInvokedMethodsByHandler(handler);
			for(String invokedMethod : invokedMethods) {
				
				String identicalMethod = this.getMatchedSignature(invokedMethod);
				String similarMethod = this.getMostSimilarSignature(invokedMethod);
				Set<String> identicalMethodActions = this.queryTriggeringAction(identicalMethod, newProcessors);
				Set<String> similarMethodActions = this.queryTriggeringAction(similarMethod, newProcessors);
				
				Utils.debugPrintln("    ==> method: " + invokedMethod);
				Utils.debugPrintln("      identical method actions: " + identicalMethodActions.size() + ",  " + identicalMethodActions);
				Utils.debugPrintln("        similar method actions: " + similarMethodActions.size() + ",  " + similarMethodActions);
				
				Set<String> actionsForMethod = null;
				if(!identicalMethodActions.isEmpty()) {
					actionsForMethod = identicalMethodActions;
				} else {
					if(similarMethodActions.isEmpty()) {
						continue;
					} else {
						actionsForMethod = similarMethodActions;
					}
				}
				Utils.checkNotNull(actionsForMethod);
				int actionNum = actionsForMethod.size();
				Float actionW = 1.0f / actionNum;
				for(String action : actionsForMethod) {
					if(weightMap.containsKey(action)) {
						if(weightMap.get(action) < actionW) {
							weightMap.put(action, actionW);
						}
					} else {
						weightMap.put(action, actionW);
					}
				}
				
			}
		}
		
		/** perform one more pass */
		for(UIActionHandler handler : noMatchingHandlers) {
			List<String> invokedMethods = oldProcessor.getInvokedMethodsByHandler(handler);
            for(String invokedMethod : invokedMethods) {
				
				String identicalMethod = this.getMatchedSignature(invokedMethod);
				String similarMethod = this.getMostSimilarSignature(invokedMethod);
				Set<String> identicalMethodActions = this.queryTriggeringAction(identicalMethod, newProcessors);
				Set<String> similarMethodActions = this.queryTriggeringAction(similarMethod, newProcessors);
				
				Set<String> actionsForMethod = null;
				if(!identicalMethodActions.isEmpty()) {
					actionsForMethod = identicalMethodActions;
				} else {
					if(similarMethodActions.isEmpty()) {
						continue;
					} else {
						actionsForMethod = similarMethodActions;
					}
				}
				Utils.checkNotNull(actionsForMethod);
				int actionNum = actionsForMethod.size();
				Float actionW = 1.0f / actionNum;
				for(String action : actionsForMethod) {
					if(weightMap.containsKey(action)) {
						if(weightMap.get(action) < actionW) {
							weightMap.put(action, actionW);
						}
					} else {
						weightMap.put(action, actionW);
					}
				}
				
			}
		}
		/*************************/
		
		List<String> retActions = new LinkedList<String>();
		Map<String, Float> sortedMap = Utils.sortByValue(weightMap, false);
		retActions.addAll(sortedMap.keySet());
		
		Utils.debugPrintln("Final results: ");
		for(String action : sortedMap.keySet()) {
			Utils.debugPrintln("  " + action + ", weight: " + sortedMap.get(action));
			Utils.debugPrintln(edu.washington.cs.workflow.util.Globals.lineSep);
		}
		
		return retActions;
	}
	
	
    public List<String> recommendActionsByProportion(String handlerDesc) {
    	MethodSummarizer summarizer = new MethodSummarizer(this.oldTraceFile, newTraceFiles.toArray(new String[0]));
    	List<String> oldMethods = summarizer.oldProcessor.getInvokedMethodsByHandler(handlerDesc);
    	
    	List<String> allNewActions = summarizer.getAllNewActionCandidates();
    	Map<String, Integer> newActionCoverage = new LinkedHashMap<String, Integer>();
    	
    	for(String newActionDesc : allNewActions) {
    		List<String> invokedMethods = summarizer.getInvokedMethodsByNewAction(newActionDesc);
    		Set<String> invokedMethodSet = new HashSet<String>(invokedMethods);
    		int count = 0;
    		for(String oldMethod : oldMethods) {
    			String matchedMethod = this.getMatchedSignature(oldMethod);
    			if(matchedMethod == null) {
    				matchedMethod = this.getMostSimilarSignature(oldMethod);
    			}
    			if(matchedMethod != null) {
    			    if(invokedMethodSet.contains(matchedMethod)) {
    				    count++;
    			    }
    			}
    		}
    		newActionCoverage.put(newActionDesc, count);
    	}
    	
    	Map<String, Integer> sortedMap = Utils.sortByValue(newActionCoverage, false);
    	
    	List<String> actions = new LinkedList<String>(sortedMap.keySet());
		return actions;
	}
	
	/**
	 * Basic logic: parse the old trace file. iterate each 
	 * */
	public List<String> recommendActions() {
		return recommendActions(true);
	}
	public List<String> recommendActions(boolean matched) {
		List<String> retActions = new LinkedList<String>();
		Set<String> uniqueActions = new LinkedHashSet<String>();
		
		EventTraceProcessor oldProcessor = new EventTraceProcessor(this.oldTraceFile);
		EventTraceProcessor[] newProcessors = new EventTraceProcessor[this.newTraceFiles.size()];
		for(int i = 0; i < newProcessors.length; i++) {
			newProcessors[i] = new EventTraceProcessor(this.newTraceFiles.get(i));
		}
		//iterate through old processor's action handlers
		List<UIActionHandler> reversedHandlerList = oldProcessor.getAllActionHandlers();
		Collections.reverse(reversedHandlerList);
		
		for(UIActionHandler handler : reversedHandlerList) {
			String matchedHandlerDesc = matched ? this.getMatchedSignature(handler.methodDesc)
					    : this.getMostSimilarSignature(handler.methodDesc);
			Set<String> handlerActions = this.queryTriggeringAction(matchedHandlerDesc, newProcessors);
			Utils.debugPrintln("  " + matched + " action size of handler: " + matchedHandlerDesc + ", " + handlerActions.size() + ",  actions: " + handlerActions
					+ ", original handler: " + handler.methodDesc);
			if(handlerActions.isEmpty()) {
				
				//do sth here
				//FIXME here, if a handler is not matched, we need to find its similar one
				String similarySignature = this.getMostSimilarSignature(handler.methodDesc);
				Set<String> similarHandlerActions = this.queryTriggeringAction(similarySignature, newProcessors);
				Utils.debugPrintln("  " + matched + " action size of similar handler: " + similarySignature + ", " + similarHandlerActions.size()
						+ ",  actions: " + similarHandlerActions + ",  original handler: " + handler.methodDesc);
				//FIXME what if it has multiple actions
				uniqueActions.addAll(similarHandlerActions);
				
				Utils.debugPrintln("Skip handlers without actions: " + handler.methodDesc);
				
				if(handler.methodDesc.equals("net.sourceforge.ganttproject.gui.previousState.GanttDialogSaveAsPreviousState.actionPerformed(Ljava/awt/event/ActionEvent;)V")) {
					//do nothing
				} else {
				    //FIXME
				    continue;
				}
			}
			//if it is the only handler
			if(handlerActions.size() == 1) {
				uniqueActions.addAll(handlerActions);
			}
			//FIXME check similarity
			if(handlerActions.size() == 2) {
				if(handlerActions.toString().indexOf("Export") != -1) {
				    uniqueActions.addAll(handlerActions);
				}
			}
			//specifically for similarity matching
			if(!matched) {
				//FIXME what if it has multiple actions
				uniqueActions.addAll(handlerActions);
			}
//			System.out.println("Handler: " + matchedHandlerDesc);
			//if it has multiple actions, look at the invoked method
			List<String> invokedMethods = oldProcessor.getInvokedMethodsByHandler(handler);
			for(String invokedMethod : invokedMethods) {
				String matchedMethod = matched ? this.getMatchedSignature(invokedMethod)
						: this.getMostSimilarSignature(invokedMethod);
				Set<String> methodActions = this.queryTriggeringAction(matchedMethod, newProcessors);
				//remove methods that can be triggered by multiple actions
				//have some problem, some method must be invoked by multiple methods
				//or sort by the uniqueness TODO
				Utils.debugPrintln("  " + matched + ", action size of method: " + matchedMethod + ", " + methodActions.size()
						+ ",  actions: " + methodActions + ", original method sig: " + invokedMethod);
				if(methodActions.size() == 1) {
					uniqueActions.addAll(methodActions);
				}
			}
		}
		
		retActions.addAll(uniqueActions);
		return retActions;
	}
	
	private String getMatchedSignature(String methodSig) {
		//Note:
		//the following two pairs are identified by mining the code repository.
		//for the convenience of evaluation, they are hard-coded here
		if(methodSig.equals("net.sf.jabref.JabRefFrame$7.actionPerformed(Ljava/awt/event/ActionEvent;)V")) {
			return "net.sf.jabref.export.ExportFormats$1ExportAction.actionPerformed(Ljava/awt/event/ActionEvent;)V";
		}
		if(methodSig.equals("freemind.modes.mindmapmode.MindMapController$EdgeStyleAction.actionPerformed(Ljava/awt/event/ActionEvent;)V")) {
			return "freemind.modes.actions.EdgeStyleAction.act(Lfreemind/controller/actions/generated/instance/XmlAction;)V";
		}
		if(matcher != null) {
			String matchedSig = matcher.findCounterpartSig(methodSig);
			if(matchedSig != null) {
				return matchedSig;
			}
		}
		return methodSig;
	}
	
	private String getMostSimilarSignature(String methodSig) {
		if(matcher != null) {
			String similarSig = matcher.findMostSimilarMethodSigByMethodName(methodSig);
			if(similarSig != null) {
				return similarSig;
			}
		}
		return methodSig;
	}
	
	protected Set<String> queryTriggeringAction(String methodDesc, EventTraceProcessor[] newProcessors) {
		Set<String> retActions = new LinkedHashSet<String>();
		
		for(EventTraceProcessor processor : newProcessors) {
			List<String> actions = processor.getTriggeringActionDescs(methodDesc);
			retActions.addAll(actions);
		}
		
		return retActions;
	}
}
